Skip to content

fix: Enforce aggregate MaximumAmount in multi-send MPT#6644

Open
Tapanito wants to merge 3 commits intodevelopfrom
tapanito/fix-multi-send-max-amount
Open

fix: Enforce aggregate MaximumAmount in multi-send MPT#6644
Tapanito wants to merge 3 commits intodevelopfrom
tapanito/fix-multi-send-max-amount

Conversation

@Tapanito
Copy link
Copy Markdown
Collaborator

rippleSendMultiMPT used a read-only SLE snapshot (view.read) to check MaximumAmount per iteration. Since rippleCreditMPT updates a separate mutable copy (view.peek), the snapshot's sfOutstandingAmount was stale after the first iteration, allowing the aggregate to exceed MaximumAmount.

Replace the per-iteration check with a running total that validates the aggregate against MaximumAmount within the send loop. The old per-iteration check is retained behind a !fixAssortedFixes gate for ledger replay compatibility.

High Level Overview of Change

Context of Change

API Impact

  • Public API: New feature (new methods and/or new fields)
  • Public API: Breaking change (in general, breaking changes should only impact the next api_version)
  • libxrpl change (any change that may affect libxrpl or dependents of libxrpl)
  • Peer protocol change (must be backward compatible or bump the peer protocol version)

rippleSendMultiMPT used a read-only SLE snapshot (view.read) to check
MaximumAmount per iteration. Since rippleCreditMPT updates a separate
mutable copy (view.peek), the snapshot's sfOutstandingAmount was stale
after the first iteration, allowing the aggregate to exceed
MaximumAmount.

Replace the per-iteration check with a running total that validates
the aggregate against MaximumAmount within the send loop. The old
per-iteration check is retained behind a !fixAssortedFixes gate for
ledger replay compatibility.
@Tapanito Tapanito added Amendment AI Triage Bugs and fixes that have been triaged via AI initiatives labels Mar 25, 2026
@github-actions
Copy link
Copy Markdown

This PR has conflicts, please resolve them in order for the PR to be reviewed.

…-send-max-amount

# Conflicts:
#	include/xrpl/protocol/detail/features.macro
#	src/libxrpl/ledger/View.cpp
@github-actions
Copy link
Copy Markdown

All conflicts have been resolved. Assigned reviewers can now start or resume their review.

@Tapanito Tapanito marked this pull request as ready for review March 26, 2026 10:51
@Tapanito
Copy link
Copy Markdown
Collaborator Author

/ai-review

Copy link
Copy Markdown
Contributor

@xrplf-ai-reviewer xrplf-ai-reviewer bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went through the changes

The core fix is correct, but four issues need attention: a uint64_t wrap-around risk in the pre-amendment arithmetic (line 1215), a semantic divergence from the original subtraction-underflow behavior that may break ledger replay fidelity (line 1215), missing // KNOWN BUG comments at the broken-behavior assertion sites in the test (line 3351), and a reminder to queue fixSecurity3_1_3 for prompt activation and audit other multi-send paths for the same stale-snapshot pattern. See inline comments.


Review by ReviewBot 🤖

Review by Claude Opus 4.6 · Prompt: V12

@codecov
Copy link
Copy Markdown

codecov bot commented Mar 26, 2026

Codecov Report

❌ Patch coverage is 93.75000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 81.5%. Comparing base (2c765f6) to head (5b3ae8c).
⚠️ Report is 5 commits behind head on develop.

Files with missing lines Patch % Lines
src/libxrpl/ledger/helpers/TokenHelpers.cpp 93.8% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##           develop   #6644   +/-   ##
=======================================
  Coverage     81.4%   81.5%           
=======================================
  Files          998     998           
  Lines        74443   74450    +7     
  Branches      7563    7558    -5     
=======================================
+ Hits         60632   60640    +8     
+ Misses       13811   13810    -1     
Files with missing lines Coverage Δ
src/libxrpl/ledger/helpers/TokenHelpers.cpp 93.6% <93.8%> (+0.1%) ⬆️

... and 1 file with indirect coverage changes

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Use subtraction-based guards instead of addition to prevent uint64_t
overflow in both the post-amendment aggregate check and the
pre-amendment per-iteration check. Each condition in the cascade
protects the subtraction in the next from underflow.

Move totalSendAmount accumulation after the check so the guard
operates on the pre-addition value.
Copy link
Copy Markdown
Contributor

@xrplf-ai-reviewer xrplf-ai-reviewer bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looked through this one

One high-severity security note flagged inline: the pre-fixSecurity3_1_3 path allows issuer to bypass MaximumAmount via stale snapshot — activate the amendment promptly.


Review by ReviewBot 🤖

Review by Claude Opus 4.6 · Prompt: V12

Copy link
Copy Markdown
Contributor

@pratikmankawde pratikmankawde left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a correctness gap in MPToken multi-destination sends where MaximumAmount could be exceeded because per-iteration checks observed a stale view.read() snapshot instead of the updated outstanding amount.

Changes:

  • Update rippleSendMultiMPT to enforce MaximumAmount using an aggregate/running-total check inside the send loop (with pre-amendment behavior retained for replay compatibility).
  • Add a unit test covering multi-send aggregate MaximumAmount enforcement, including a pre-amendment “known bug” case.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/libxrpl/ledger/helpers/TokenHelpers.cpp Implements aggregate MaximumAmount enforcement for issuer multi-send, with amendment gating for replay compatibility.
src/test/app/MPToken_test.cpp Adds a regression test validating correct aggregate enforcement and preserving pre-amendment behavior expectations.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +1167 to +1214
Number totalSendAmount;
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
auto const outstandingAmount = sle->getFieldU64(sfOutstandingAmount);

// actual accumulates the total cost to the sender (includes transfer
// fees for third-party transit sends). takeFromSender accumulates only
// the transit portion that is debited to the issuer in bulk after the
// loop. They diverge when there are transfer fees.
STAmount takeFromSender{mptIssue};
actual = takeFromSender;

for (auto const& r : receivers)
for (auto const& [receiverID, amt] : receivers)
{
auto const& receiverID = r.first;
STAmount amount{mptIssue, r.second};
STAmount const amount{mptIssue, amt};

if (amount < beast::zero)
{
return tecINTERNAL; // LCOV_EXCL_LINE
}

/* If we aren't sending anything or if the sender is the same as the
* receiver then we don't need to do anything.
*/
if (!amount || (senderID == receiverID))
if (!amount || senderID == receiverID)
continue;

if (senderID == issuer || receiverID == issuer)
{
// if sender is issuer, check that the new OutstandingAmount will
// not exceed MaximumAmount
if (senderID == issuer)
{
XRPL_ASSERT_PARTS(
takeFromSender == beast::zero,
"xrpl::rippleSendMultiMPT",
"sender == issuer, takeFromSender == zero");

auto const sendAmount = amount.mpt().value();
auto const maximumAmount = sle->at(~sfMaximumAmount).value_or(maxMPTokenAmount);
if (sendAmount > maximumAmount ||
sle->getFieldU64(sfOutstandingAmount) > maximumAmount - sendAmount)
return tecPATH_DRY;

if (view.rules().enabled(fixSecurity3_1_3))
{
// Post-fixSecurity3_1_3: aggregate MaximumAmount
// check. Each condition guards the subtraction
// in the next to prevent underflow.
auto const exceedsMaximumAmount =
// This send alone exceeds the max cap
sendAmount > maximumAmount ||
// The aggregate of all sends exceeds the max cap
totalSendAmount > maximumAmount - sendAmount ||
// Outstanding + aggregate exceeds the max cap
outstandingAmount > maximumAmount - sendAmount - totalSendAmount;

if (exceedsMaximumAmount)
return tecPATH_DRY;
totalSendAmount += sendAmount;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

totalSendAmount is tracked as Number, but this MaximumAmount enforcement needs exact integer arithmetic. Number precision depends on mantissa scale (small vs large), and in small scale values near maxMPTokenAmount can lose unit precision, potentially allowing a small aggregate overflow or false rejection. Consider using an exact integral type here (e.g. std::uint64_t or MPTAmount) and keep the comparisons/subtractions in unsigned 63-bit arithmetic, similar to the existing per-iteration check.

Copilot uses AI. Check for mistakes.
Comment on lines +1199 to +1224
if (view.rules().enabled(fixSecurity3_1_3))
{
// Post-fixSecurity3_1_3: aggregate MaximumAmount
// check. Each condition guards the subtraction
// in the next to prevent underflow.
auto const exceedsMaximumAmount =
// This send alone exceeds the max cap
sendAmount > maximumAmount ||
// The aggregate of all sends exceeds the max cap
totalSendAmount > maximumAmount - sendAmount ||
// Outstanding + aggregate exceeds the max cap
outstandingAmount > maximumAmount - sendAmount - totalSendAmount;

if (exceedsMaximumAmount)
return tecPATH_DRY;
totalSendAmount += sendAmount;
}
else
{
// Pre-fixSecurity3_1_3: per-iteration MaximumAmount
// check. Reads sfOutstandingAmount from a stale
// view.read() snapshot — incorrect for multi-destination
// sends but retained for ledger replay compatibility.
if (sendAmount > maximumAmount ||
outstandingAmount > maximumAmount - sendAmount)
return tecPATH_DRY;
Copy link

Copilot AI Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description mentions retaining the old behavior behind a !fixAssortedFixes gate, but the implementation is gated on fixSecurity3_1_3. Since fixAssortedFixes doesn't appear to exist in the codebase, please update the PR description (or rename the gate if that was the intent) to avoid confusion for reviewers and future archaeology.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

AI Triage Bugs and fixes that have been triaged via AI initiatives Amendment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants